package edu.gatech.cc.liam.geometry.linear.convexhull;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import edu.gatech.cc.liam.geometry.linear.Halfspace;
import edu.gatech.cc.liam.geometry.linear.NPoint;
import edu.gatech.cc.liam.geometry.linear.PointSet;
import edu.gatech.cc.liam.geometry.linear.Polytope;
 
public class QHull {

	//static public final double APPROXBOUND = 0.95;
	static private final boolean ERROR_PRINTS = false;
	
	static public void qConvexHullExact(PointSet destination, PointSet inputSet) {
		qConvexHullIntoDest(destination, null, inputSet, "");		
	}
	static public void qConvexHullExact(PointSet destPoints, Polytope destTope, PointSet inputSet) {
		qConvexHullIntoDest(destPoints, destTope, inputSet, "");		
	}
	static public void qConvexHull(PointSet destination, PointSet inputSet, double APPROXBOUND) {
		//qConvexHullIntoDest(destination, inputSet, "A-" + APPROXBOUND + " p");
		qConvexHullIntoDest(destination, null, inputSet, "U" + APPROXBOUND + " A-" + (1.0-APPROXBOUND) +" p");
	}
	
//	static private void qConvexHullIntoDestOLD(PointSet destination, PointSet inputSet, String execParams) {
//		//System.out.print("" + inputSet.size() + "-QH-");
//		System.out.print(".");
//		PointSet newHull = qConvexHullHelper(inputSet, execParams);
//		destination.clear();
//		destination.addAll(newHull);
//		//System.out.print(destination.size() + " ");
//		assert destination.size() != 0;
//	}

	static private void qConvexHullIntoDest(PointSet destPoints, Polytope destTope, PointSet inputPoints, String execParams) {
		PointSet inputSet = inputPoints.clone();
		if(inputSet.size() <= 2 && destTope == null) {
			destPoints.clear();
			destPoints.addAll(inputSet);
		} else {
			int dimensions = inputSet.iterator().next().values.length;
			boolean success = qHullCall(destPoints, destTope, inputSet, Preferences.QCONVEX_EXECUTABLE + " " + execParams);
			if(!success) {
				Set<NPoint> fakePoints = new HashSet<NPoint>();
				PointSet newInputSet = inputSet.clone();
				double[] min = inputSet.getMinBounds();
				double[] max = inputSet.getMaxBounds();
				for(int i=0; i<min.length; i++) {
					if(max[i] - min[i] <= 0.001) {
						max[i] = max[i] + Math.abs(max[i]*9) + 0.001;
						min[i] = min[i] - Math.abs(min[i]*0.9) - 0.001;
					}
				}
				while(!success) {
					// add points to make the polytope full dimension (required by qhull)
					NPoint randPt = NPoint.makeLinearRandom(dimensions, min, max);
					fakePoints.add(randPt);
					newInputSet.add(randPt);
					success = qHullCall(destPoints, destTope, newInputSet, Preferences.QCONVEX_EXECUTABLE + " " + execParams);
				}
				if(destPoints != null) {
					Set<NPoint> fakePointsToBeRemoved = new HashSet<NPoint>();
					for(NPoint bogusPt : fakePoints) {
						for(NPoint pt : destPoints) {
							if(pt.softEquals(bogusPt))
								fakePointsToBeRemoved.add(pt);
						}
					}
					destPoints.removeAll(fakePointsToBeRemoved);
				}
				if(destTope != null)
					destTope.addAll(inputSet.getManifold());
			}
		}
	}

	static private boolean qHullCall(PointSet destPoints, Polytope destTope, PointSet inputSet, String executeString) {
		if(inputSet.size() <= inputSet.dimensions())
			return false;
		boolean success = true;
		if(destPoints != null)
			executeString += " p";
		if(destTope != null)
			executeString += " n";
		try
		{
			Runtime rt = Runtime.getRuntime() ;
			Process p = rt.exec(Preferences.QHULL_LOCATION + Preferences.fileSeparator + executeString + " Pp Tz") ;
			BufferedWriter input = new BufferedWriter(new OutputStreamWriter(p.getOutputStream()) );
			//InputStream output = p.getInputStream() ;
			//OutputStream input = p.getOutputStream ();
			//InputStream err = p.getErrorStream() ;
			sendInputString(inputSet, input);
			//input.write(inputStr);
			input.close();
			//String badInput = makeInputStringDeprecated(inputSet);

			BufferedReader output = new BufferedReader(new InputStreamReader(p.getInputStream()));
			if(destPoints != null) {
				destPoints.clear();
				PointSet setOutput = parseOutputAsPointSet(output);
				destPoints.addAll(setOutput);
				if(destPoints.isEmpty())
					success = false;
			}
			if(destTope != null) {
				destTope.clear();
				destTope.addAll(parseOutputAsPolytope(output));
				if(destTope.isEmpty())
					success = false;
			}
			output.close();

			p.destroy();
		}catch(Exception exc){
			if(ERROR_PRINTS) {
				try {
					sendInputString(inputSet, new BufferedWriter(new OutputStreamWriter(System.err)));
					System.out.flush();
				} catch (IOException e) {
					System.err.println("IOException in printing input to QHULL");
				}
			}
			exc.printStackTrace();
			success = false;
			throw new Error();
		}
		return success;
	}

	 static private void sendInputString(Collection<NPoint> inputSet, Writer input) throws IOException {
		Integer dim = null;
		for (NPoint aPoint : inputSet){
			if (dim == null) {
				dim = aPoint.values.length;
				printAndWrite(input, dim + "\n" + inputSet.size() + "\n");
				//input.write(dim + "\n" + inputSet.size() + "\n");
			}
			else assert dim == aPoint.values.length;
			for (double d : aPoint.values) {
				printAndWrite(input, d + " ");
				//input.write(d + " ");
			}
			printAndWrite(input, "\n");
			//input.write("\n");
		}
	}

	static private void printAndWrite(Writer input, String str) throws IOException {
		//System.out.print(str);
		input.write(str);
	}

	static protected PointSet parseOutputAsPointSet(BufferedReader output) {
		PointSet returnedPoints = new PointSet();
		String lineOfOutput = "";
		try {
			lineOfOutput = output.readLine();
			int numDimensions = Integer.parseInt(lineOfOutput);
			lineOfOutput = output.readLine();
			int numPoints = Integer.parseInt(lineOfOutput);
			for (int ptNum=0; ptNum<numPoints; ptNum++) {
				lineOfOutput = output.readLine();
				assert lineOfOutput != null;
				NPoint newPoint = NPoint.parseSpaceSeperatedNPoint(lineOfOutput);
				assert newPoint.dimensions() == numDimensions;
				returnedPoints.add(newPoint);
			}
		}
		catch(NumberFormatException nfe)
		{
			if(ERROR_PRINTS)
				reportParseError(output, lineOfOutput);
			//returnedPoints = null;
		} catch(Exception exc){
			System.err.println("Call to QHULL failed");
			exc.printStackTrace();
			throw new Error();
		}
		return returnedPoints;
	}
	
	static protected Polytope parseOutputAsPolytope(BufferedReader output) {
		 Polytope returnedTope = new Polytope();
		String lineOfOutput = "";
		try {
			lineOfOutput = output.readLine();
			int numDimensions = Integer.parseInt(lineOfOutput);
			lineOfOutput = output.readLine();
			int numHalfspaces = Integer.parseInt(lineOfOutput);
			for (int hsNum=0; hsNum<numHalfspaces; hsNum++) {
				lineOfOutput = output.readLine();
				assert lineOfOutput != null;
				NPoint newPoint = NPoint.parseSpaceSeperatedNPoint(lineOfOutput);
				assert newPoint.dimensions() == numDimensions;
				returnedTope.add(new Halfspace(newPoint.values));
			}
		}
		catch(NumberFormatException nfe)
		{
			if(ERROR_PRINTS)
				reportParseError(output, lineOfOutput);
			//returnedTope = null;
		} catch(Exception exc){
			System.err.println("Call to QHULL failed");
			exc.printStackTrace();
			throw new Error();
		}
		return returnedTope;
	}

	
	private static void reportParseError(BufferedReader output, String lineOfOutput) throws Error {
		System.err.println("QHull - Couldn't Parse Output. Got: \n");
		System.err.println(lineOfOutput);
		try {
			while ((lineOfOutput = output.readLine()) != null) {
				System.err.println(lineOfOutput);
			}
		} catch (IOException e) {
			e.printStackTrace();
			throw new Error();
		}
	}

}
